feat(fe): switch OpenID callback to response_mode=form_post#4015
Conversation
The OAuth callback previously used response_mode=fragment: the IdP
redirected back with the id_token in the URL hash, which Apple Sign In
drops name/email claims under, Okta/Auth0 handle inconsistently for
hybrid flows, and OAuth 2.1 removes entirely. The id_token also ended
up visible in the callback URL.
The IdP now POSTs {id_token, state} to /callback. The frontend canister
upgrades the POST to update mode and translates it into a certified HTML
page whose single, CSP-hash-pinned inline script delivers the payload to
the frontend: via BroadcastChannel for popup flows, via sessionStorage +
/authorize?flow=openid-resume for 1-click same-tab flows (discriminated
by the ii-openid-authorize-state marker, since in the authorize flow the
tab itself is a popup opened by the relying party). RFC 6749 error
reports ({error, error_description, state}) travel through the same page
so a misconfigured SSO app still surfaces its own message as
OAuthProviderError instead of a generic failure.
The POST arrives anonymously (the IdP submits the form), so the handler
cannot redeem the JWT: the salt + nonce + caller() binding still happens
through the existing signed-ingress flow from the frontend.
In hot-reload dev, a server hook performs the same translation since no
canister fronts the dev server; SvelteKit's CSRF origin check is disabled
as it would reject the IdP's cross-origin form POST before the hook runs
(adapter-static ships no server in production, so the check only ever
applied to the dev server). In NO_HOT_RELOAD e2e, the vite forward plugin
already routes the POST to the real canister handler.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The IdP's form_post response is answered by the canister directly, so the /callback route never renders: its GET page, the sendUrlToOpener helper and the layout's origin-redirect exclusion for the path are dead. The live popup utilities (redirectInPopup, CallbackPopupClosedError, CallbackPayload) move from the route folder into $lib/utils/openID.ts next to their only consumers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
✅ No security or compliance issues detected. Reviewed everything up to 115afa6. Security Overview
Detected Code Changes
|
There was a problem hiding this comment.
Pull request overview
This PR updates the Internet Identity frontend OpenID/OAuth callback handling to use response_mode=form_post instead of URL fragments, enabling stricter OIDC provider compatibility (Okta/Auth0) and avoiding id_token exposure in callback URLs. It adds a certified canister-side /callback POST translator that delivers {id_token,state} (or RFC6749 errors) to the SPA via BroadcastChannel (popup) or sessionStorage (same-tab resume), with dev-server parity via a SvelteKit hook.
Changes:
- Add a canister update-path (
http_requestupgrade →http_request_update) to translatePOST /callbackform bodies into a certified HTML landing page. - Update frontend OpenID utilities and authorize-resume flow to consume the structured callback payload (instead of parsing URL fragments).
- Adjust dev hot-reload behavior to translate
/callbackPOSTs inhooks.server.tsand disable SvelteKit CSRF origin checking to allow cross-origin IdP form POSTs in dev.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| svelte.config.js | Disables SvelteKit CSRF origin check to allow cross-origin POST /callback in dev. |
| src/internet_identity_frontend/tests/integration/http.rs | Adds PocketIC integration coverage for callback upgrade + translation behaviors. |
| src/internet_identity_frontend/src/main.rs | Adds upgrade-to-update routing for POST /callback and introduces http_request_update. |
| src/internet_identity_frontend/src/callback.rs | New bounded parser + certified HTML landing page generator for response_mode=form_post. |
| src/internet_identity_frontend/internet_identity_frontend.did | Exposes http_request_update in the canister interface. |
| src/internet_identity_frontend/Cargo.toml | Adds form_urlencoded dependency for parsing callback form bodies. |
| src/frontend/src/routes/+layout.svelte | Removes legacy /callback exemption from client-side redirect logic. |
| src/frontend/src/routes/(new-styling)/callback/utils.ts | Deletes legacy fragment-era callback popup utilities. |
| src/frontend/src/routes/(new-styling)/callback/+page.svelte | Deletes legacy fragment-era callback page. |
| src/frontend/src/routes/(new-styling)/authorize/+page.svelte | Updates same-tab resume flow to read payload from sessionStorage and validate via extractIdTokenFromCallback. |
| src/frontend/src/lib/utils/openID.ts | Moves popup utilities here; requests form_post; validates structured callback payload. |
| src/frontend/src/lib/utils/openID.test.ts | Updates unit tests for structured payload parsing and form_post redirect URL creation. |
| src/frontend/src/hooks.server.ts | Implements dev-server stand-in for canister /callback translation. |
| src/canister_tests/src/api.rs | Adds http_request_update helper to simulate HTTP gateway upgrade behavior. |
| Cargo.lock | Records the new form_urlencoded dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace the hand-rolled Reflect.get-based property reader and shape guard with Zod schemas, matching how the rest of the frontend validates untrusted boundary data (e.g. the ICRC channel handlers, ssoDiscovery, auth-handoff). isCallbackPayload becomes a strict union safeParse; extractIdTokenFromCallback reads fields through a lenient schema that still runs the CSRF state check first and treats any non-string field as absent. The schema accepts the JSON null the canister emits for an absent error_description and normalizes it to undefined. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses Copilot review on #4015. The /callback landing page inherited the SPA-wide CSP, whose `script-src 'self' 'unsafe-inline' 'unsafe-eval'` (needed for SvelteKit and agent-js wasm) left the computed inline-script hash doing nothing unless the browser applied the CSP3 rule that a hash disables 'unsafe-inline'. Give the page its own policy instead — `default-src 'none'; script-src 'sha384-...'; base-uri 'none'; frame-ancestors 'none'` (error page: no script-src) — so the hash actually governs execution and 'unsafe-eval'/'self' are gone. The SPA-wide CSP is replaced on this response, not appended. Also narrow the 405 `Allow` header from `GET, POST` to `GET`: method_not_allowed is only reached for non-/callback resources, which serve GET only (POST is handled on /callback via the query->update upgrade). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @copilot — both addressed in 50a8af1:
|
…eimplementing it The dev-server hook reimplemented the form_post → HTML translation in TypeScript, duplicating the canister's callback.rs (escaping, the flow-discriminator script, the payload shape). The frontend canister is always installed when working on OpenID, and the hook already round-trips to it to recover the injected <body> tag — so instead forward the /callback POST to the deployed canister and return its certified response. Dev now exercises the real translator; there is a single source of truth. Also migrate the CSRF escape hatch from the now-deprecated `csrf.checkOrigin: false` to `csrf.trustedOrigins: ['*']`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- redirect malformed callbacks to a new in-SPA /callback-error page instead of returning inline HTML (dev hook passes the 303 through) - serialize the payload with a serde derive on CallbackPayload - thread a CSP override through dynamic_response_headers instead of filtering out and re-adding the header in the callback module - report the observed size and the limit in parser bound errors - property-test the charset predicates and the JSON embedding - drop the .did comment, rename the JSON data-block id to callback-payload, restructure the parser into guard clauses, and remove stale historical comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the one conflict in src/internet_identity_frontend/src/main.rs: main added an `mcp_server_origin: Option<&str>` parameter to get_asset_headers / get_content_security_policy, while this branch added a `content_security_policy_override: Option<String>`. Combined both: get_asset_headers now takes both, mcp_server_origin is persisted in HeaderConfig so dynamic_response_headers can pass it, and the override short-circuits the computed CSP (which now also threads mcp_server_origin).
Sign-in with Apple silently loses the user's name and email, and some strict OIDC providers (Okta, Auth0) reject our hybrid-flow callback (by default): both are artifacts of
response_mode=fragment, which OAuth 2.1 removes entirely and which also exposes the id_token in the callback URL. This implements Track A (§7) of the OpenID/SSO production-readiness design: the IdP now delivers the OAuth response withresponse_mode=form_postinstead.Changes
{id_token, state}to/callback; the frontend canister upgrades the POST to update mode (so the response is certified) and returns an HTML page whose single, CSP-hash-pinned inline script delivers the payload to the frontend: viaBroadcastChannelfor popup flows, via sessionStorage +/authorize?flow=openid-resumefor 1-click same-tab flows. The flows are discriminated by theii-openid-authorize-statemarker —window.openercannot tell them apart since the authorize tab is itself a popup opened by the relying party.caller()JWT redemption binding still happens through the existing signed-ingress flow.{error, error_description, state}) travel through the same page, so a misconfigured SSO app still surfaces its own message asOAuthProviderErrorinstead of a generic failure.303redirect to a new in-SPA error page (/callback-error) that renders the styled, localized error UI; the machine reason rides along as a query param to aid debugging a misconfigured IdP.content_security_policy_overrideparameter on the shared header builder, rather than carving the CSP out of the SPA-wide headers.createRedirectURLrequestsform_post;extractIdTokenFromCallbackvalidates the structured payload (state check first);resumeOpenIdreads the payload from sessionStorage.303through unfollowed; SvelteKit's CSRF origin check is disabled since it would reject the IdP's cross-origin form POST before the hook runs — adapter-static ships no server in production, so the check only ever applied to the dev server. In NO_HOT_RELOAD e2e the vite forward plugin routes the POST to the real canister handler.$lib/utils/openID.ts.Tests
</>/&and round-trips)./callbackupgrade flag, success/error translation, malformed-body redirect to/callback-error, security headers, 405s.createRedirectURLform_post coverage.🤖 Generated with Claude Code